package Cluedo.Controller;
import java.util.Vector;
import Cluedo.API.*;
import Cluedo.Game.*;
import Cluedo.AI.*;

/**
 * GameController is responsible for dealing with requests to change the GameModel.<br>
 * It notifies its listeners if a change is made whereby the view must update itself.<p>
 *
 * directCommand is called from any view.
 *
 * @author alex
 */
public class GameController implements Controller {
	/**
	 * Instance of the current game/board.
	 */
	public Cluedo.Game.Board board = null;
	
	
	/**
	 * References to all listeners (views)
	 * 
	 */
	Vector views = new Vector();
	
	/**
	 * Putting this here to keep threads alive
	 *
	 */
	Vector AIThreads = new Vector();
	
	
	public void setAIs( Vector AIThreads ) {
		this.AIThreads  = AIThreads;
	}

	public Vector getAIs( Vector AIThreads ) {
		return AIThreads;
	}
	
	int debugLevel = 0;
	
	/**
	 * Return a pointer link to the current game's board ( the model )
	 *
	 * @return a link to the current game's board, null if no game exists.
	 */
	public Cluedo.Game.Board getBoard() {
		return board;	
	}
	
	
	/**
	* Creates new GameController
	*/
	public GameController() {
		createBoard(null);
	}

	/**
	* Sends a message to all listeners.
	*
	*/
	public void notifyAllListeners(Message feedback) {
		StringBuffer output = new StringBuffer();
		output.append("\n[GC:notifyAllListeners] @ "  + views.size() + "\n");
		try {
			for(int i = 0; i < views.size(); i++) {

				if(views.elementAt(i) instanceof Cluedo.API.CluedoListener) {
					( (Cluedo.API.CluedoListener) views.elementAt(i) ).notifyAlert(feedback);
					output.append(
						views.elementAt(i)+ "\n" +
						 "\t" +views.elementAt(i).getClass()  + "\n");
				}

			}
		}
		catch(Exception listenerError) {
			System.out.println("[GameController:notifyAllListeners] unable to send messages.");
			System.out.println("Possible cause: -\n");
			listenerError.printStackTrace();
		}
		if( debugLevel > 3 ) {
			System.out.println(output.toString());
		}
	}
	
	/**
	* Add a view to listen to this GameController.
	* @param view
	*	 viewer to be added
	*/
	public void bindListener(Object view) throws NullPointerException{
		try {
			if(view == null)
				throw new NullPointerException();
			if(!views.contains(view)) {
				views.add(view);
				if(debugLevel > 2)
					System.out.println("["+this+":bindListener] "+view+".");	
			}
		}
		catch (Exception error) {
			System.out.println("GameController:bindListener] error in view object.");
		}
	}


	/**
	 *
	 */
	public void relayMessage(Message alert) {
		notifyAllListeners(alert);
	}
	
	/**
	* Allows a view to tell the controller to change something in the model.
	* 
	* @param parameters
	*        a vector of any data to be used in the model update
	* @param directCommand
	*		the command/message to be acted upon.
	*
	*/
	public void directCommand(String directCommand, Vector parameters) {
		Message feedback = null;
		try {
			if(directCommand.equals("createBoard")) {
					feedback = createBoard(parameters);
			}
			else if(directCommand.equals("loadNewDetectivePads")) {
				feedback = loadNewDetectivePads();
			}
			else if(directCommand.equals("insertPlayers")) {
					feedback = insertPlayers(parameters);
			}
			else if(directCommand.equals("insertSquares")) {
					feedback = insertSquares(parameters);
			}
			else if(directCommand.equals("createBindings")) {
					feedback = createBindings(parameters);
			}
			else if(directCommand.equals("insertCards")) {
					feedback = insertCards(parameters);
			}
			else if(directCommand.equals("AIPlayersOn")) {
					feedback = AIPlayersOn();
			}
			else if(directCommand.equals("roll")) {
				System.out.println("[GC:directCommand] Dice roll request returned... ");
				feedback = rollDice();
					System.out.println("[GC:directCommand] Dice roll returned... ");
			}
			else if(directCommand.equals("unmarkDetectivePad")) {
				if(parameters.size() == 2)
					feedback = unmarkDetectivePad( ((String)parameters.firstElement()), ((Player)parameters.elementAt(1)));
				else
					feedback = unmarkDetectivePad( ((String)parameters.firstElement()), null);
			}			
			else if(directCommand.equals("markDetectivePad")) {
				if(parameters.size() == 2)
					feedback = markDetectivePad( ((String)parameters.firstElement()), ((Player)parameters.elementAt(1)));
				else
					feedback = markDetectivePad( ((String)parameters.firstElement()), null);
			}
			else if(directCommand.equals("markSuggestMade")) {
//				if(parameters.size() == 2)
				feedback = markSuggestMade( ((Player)parameters.firstElement()) );
				//else
//					feedback = markSuggestMade( ((Card)parameters.firstElement()), null);

			}
			else if(directCommand.equals("dealCardsToPlayers")) {
					feedback = dealCardsToPlayers();
			}
			else if(directCommand.equals("commenceGame")) {
					feedback = commenceGame();
			}
			else if(directCommand.equals("winGame")) {
				//winGame
				if(parameters != null && parameters.size() > 0)
					feedback = winGame((Player)parameters.firstElement());
			}
			else if(directCommand.equals("setSuggestionOnLastTurn")) {
				feedback = setSuggestionOnLastTurn();
			}
			else if(directCommand.equals("positionPlayers")) {
				feedback = positionPlayers((String)parameters.elementAt(0));
			}
			else if(directCommand.equals("setEnvelopeCards")) {
					feedback = setEnvelopeCards(parameters);
			}
			else if(directCommand.equals("restoreEnvelopeCards")) {
				feedback = restoreEnvelopeCards((CardCollection)parameters.firstElement());
		}
			else if(directCommand.equals("removeInteractive")) {
				feedback = removeInteractive();
			}
			else if(directCommand.equals("eliminateCard")) {
					feedback = eliminateCard((String)parameters.elementAt(0));
			}
			else if(directCommand.equals("uneliminateCard")) {
					feedback = uneliminateCard((String)parameters.elementAt(0));
			}
			else if(directCommand.equals("movePlayer")) {
				if(parameters.size() == 3)
					feedback = movePlayer((Position)parameters.elementAt(0), (Player)parameters.elementAt(1),  (Vector)parameters.elementAt(2));
				else if(parameters.size() == 2)
					feedback = movePlayer((Position)parameters.elementAt(0), (Player)parameters.elementAt(1), new Vector());
			}
			else if(directCommand.equals("nextPlayer")) {
					feedback = nextPlayer();
			}
			else if (directCommand.equals("chooseFirstPlayer")) {
					feedback = chooseFirstPlayer();
			}
			else if (directCommand.equals("restorePlayerTurn")) {
				if(parameters != null && parameters.size() > 0) {
					feedback = restorePlayerTurn(((Integer)parameters.firstElement()).intValue());
				}
				else {
					System.out.println("[GameController:directCommand] restorePlayerTurn passed with invalid arguments." );
				}

			}
			else if (directCommand.equals("setGameType")) {
				feedback = setGameType(((Integer)parameters.firstElement()).intValue());
		}
/*			else if (directCommand.equals("restorePositions")) {
					feedback = restorePositions();			
			}*/
		
		
		}		
		catch(Exception functionalError) {
			System.out.println("[GameController:directCommand] " + directCommand + " caused an exception " );
			functionalError.printStackTrace() ;
		}

		// If there is a message send it to all the views, otherwise don't bother them
		if(feedback != null)
			notifyAllListeners(feedback);

		if(feedback != null) {
			if(feedback.type.equals("newRoll")) {
				System.out.println("[GC:directCommand] newRoll sent to all listeners... ");
			}
			if(feedback.type.equals("newRoll")) {
				System.out.println("[GC:directCommand] Next player to all listeners... ");
			}
		}
	}

	


	/**
     * @param i
     * @return
     */
    private Message setGameType(int type) {
        
        getBoard().setGameType(type);
        return null;
    }

    private Message removeInteractive() {
		Message feedback = new Message();
		
		board.getCurrentPlayer().setInteractive(false);
		
		
		feedback.type = "playerEliminated"; 

		return feedback;
	}
	/**
	 * Used to make the "fake" positions restored by the game loader into pointers to the real nodes.
	 *
	 */
/*	private Message restorePositions() {
		Message feedback = null;
		Vector players = board.getPlayers();

		for( int i = 0; i < players.size(); i++) {
			Player player = (Player)players.elementAt(i);
			
			
			if(board.map.findByID( player.getPosition().getID() ) instanceof Room) {
				player.setPosition ( board.map.findByID( player.getPosition().getID() ) );
				((Room)player.getPosition()).enterPlayer(player);
			}
			else if(board.map.findByID( player.getPosition().getID() ) instanceof StartingSquare) {
				player.setPosition( board.map.findByID( player.getPosition().getID()) );
				((StartingSquare)player.getPosition()).enterPlayer(player);
			}
			else {
				player.setPosition( board.map.findByID( player.getPosition().getID()) );
				((Square)player.getPosition()).enterPlayer(player);
			}
			System.out.println("[GameController:restorePositions] " + player.getRealName() + " now at " + player.getPosition().getID() );
		}

		return feedback;
	}*/
	
	
	private Message restorePlayerTurn(int currentPlayerIndex) {
		Message feedback = null;

		board.setCurrentPlayer(currentPlayerIndex);

		return feedback;
	}
	/**
	 * Simulates the "rolling of the dice" for the player who goes first in the game.
	 *
	 */
	private Message chooseFirstPlayer() {
		Message feedback = null;
		int randomIndex =  (int) (Math.random() * board.countPlayers());
		board.setCurrentPlayer(randomIndex);

		return feedback;
	}
	
	/**
	 * Useful update tells the listener exactly when the game begins.
	 */
	private Message commenceGame() {
		Message feedback = new Message();
		
		feedback.type = "commenceGame";
		
		return feedback;
	}

	/**
	 * Wakes up all AI players, sets them to "ON".
	 *
	 */
	private Message AIPlayersOn() {
		Message feedback = null;

		System.out.println("AIPlayersOn()");
		for(int i = 0; i < views.size(); i++) {
			if(views.elementAt(i) instanceof SimpleAI || views.elementAt(i) instanceof ComplexAI
				|| views.elementAt(i) instanceof CustomAI1 || views.elementAt(i) instanceof CustomAI2
				|| views.elementAt(i) instanceof CustomAI3)
				((AIContract)views.elementAt(i)).initaliseAI();
		}
		return feedback;
	}	


	private Message unmarkDetectivePad( String cardName, Player currentPlayer ) {
		Message feedback = new Message();
		
		if(currentPlayer == null)
			currentPlayer = board.getCurrentPlayer();

		CardCollection dPadCards = currentPlayer.getDetectivePad().getCardList();
		
		if(debugLevel > 0) {
			System.out.println("[GameController:markSuggestMade] Unmark " + cardName + " " + currentPlayer.getRealName()
					+ " " + ( dPadCards.cardByName(cardName).isEliminated()==true));
		}
		
		dPadCards.unMarkCard( cardName );


		feedback.type = "cardMarked";
		feedback.data = cardName;

		feedback = null;
		return feedback;
	
	}

	private Message markDetectivePad( String cardName, Player currentPlayer ) {
		Message feedback = new Message();
		
		if(currentPlayer == null)
			currentPlayer = board.getCurrentPlayer();

		CardCollection dPadCards = currentPlayer.getDetectivePad().getCardList();
		
		if(debugLevel > 0) {
			System.out.println("[GameController:markSuggestMade] Mark " + cardName + " " + currentPlayer.getRealName()
					+ " " + ( dPadCards.cardByName(cardName).isEliminated()==true));
		}
		
		dPadCards.markCard( cardName );


		feedback.type = "cardMarked";
		feedback.data = cardName;
		feedback = null;
		return feedback;
	
	}


	/**
	 * Auto marking/eliminating of cards is called when a player is shown a card.
	 * This also marks the player as having made a suggestion on the last turn.
	 *
	 */
	private Message markSuggestMade(Player currentPlayer) {
		Message feedback = new Message();

		currentPlayer.setSuggestionOnLastTurn( true );
		feedback.type = "playerSuggestMade";
		feedback.data = currentPlayer.getRealName();

		return feedback;
	}

	/**
	 * This also marks the player as having made a suggestion on the last turn.
	 *
	 */
	private Message setSuggestionOnLastTurn( ) {
		Message feedback = new Message();
		
		
		Player currentPlayer = board.getCurrentPlayer();
		currentPlayer.setSuggestionOnLastTurn( true );
		
		feedback.type = "suggestionOnLastTurn";
		feedback.data = currentPlayer.getRealName();

		return feedback;
	}
	
	/**
	 * Boardcasts that the game has finished.
	 */
	private Message winGame(Player p) {
		Message feedback = new Message();
		
		feedback.parameters.add(p);

		feedback.type = "winGame";
		board.setGameFinished(true);
		
		return feedback;
	}
	
	
	
	/**
	 * Creates a detective pad for each player with new copies of all the game cards.
	 *
	 **/
	private Message loadNewDetectivePads( ) {
		Message feedback = null;
		Vector players = board.getPlayers();
		
		Player currentPlayer = null;
		
		// Unique copies of each card
		for(int i = 0; i < players.size(); i++) {
			currentPlayer = (Player)players.elementAt(i);
			DetectivePad dpad = new DetectivePad();
			dpad.setCardBag(board.getCardPack());
			currentPlayer.setDetectivePad(dpad);
		}

		return feedback;
	}
		
	/**
	 * Moves a player from his current position to a new position, no checks
	 * are made at this stage. Old and new positions are updated accordingly
	 * with the player's arrival/departure.
	 *
	 **/
	private Message movePlayer( Position position, Player player, Vector path ) {
		Message feedback = new Message();
		
		Position oldPosition = player.getPosition();
		
		// This makes sure the last room/square recognises ths player left it.
		if(oldPosition instanceof Room)
			((Room)oldPosition).leavePlayer(player);
		else if(oldPosition instanceof Square)
			((Square)oldPosition).leavePlayer();
	
		// "Move" the player
		player.setPosition(position);
		
		// Make sure the player can't move again until his next turn.
		player.setMoved(true);
		player.setRolled(true);
		
		// The new room/square will be alerted that the player has entered
		if(position instanceof Room)
			((Room)position).enterPlayer(player);
		else {
			((Square)position).enterPlayer(player);
			//System.out.println("Square entered: " + ((Square)position).getID()+" "  + ((Square)position).isOccupied());
		}

		// The moveAlert updates any listeners.		
		feedback.type = "moveAlert";
		feedback.data = player.getRealName();
		feedback.parameters.add(player);
		feedback.parameters.add(path);
		
		return feedback;
	}
	
	/**
	 * Sets the cards for the game.
	 *
	 */
	private Message insertCards(Vector parameters) {
			Message feedback = null;

			board.setCardPack((CardCollection)parameters.elementAt(0));

			return feedback;
	}

	/**
	 * Rolls the dice for the current player then returns a notifcation message.
	 * Also marks player as having rolled.
	 */
	private Message rollDice( ) {
		Message feedback = new Message();

		Player currentPlayer = board.getCurrentPlayer();

		board.rollDice();

		currentPlayer.setMovesRemaining( (board.diceThrow1 + board.diceThrow2) );
		currentPlayer.setRolled(true);

		System.out.println("[GameController:newRoll] "+currentPlayer.getRealName()+" == "+(board.diceThrow1 + board.diceThrow2));
		feedback.type = "newRoll";
		feedback.data = ""+(board.diceThrow1 + board.diceThrow2);

		return feedback;
	}
	
	/**
	 * Sets the nodes for the game loaded in from the CluedoConfig.
	 *
	 */	
	private Message insertSquares( Vector parameters ) {
		Message feedback = null;

		board.createSquares(parameters);

		return feedback;
	}
	
	/**
	 * Since node bindings can't be made while the bindings are loaded from disk to memory
	 * in the CluedoConfig class: we bind the nodes together later.
	 *
	 */
	private Message createBindings( Vector parameters ) {
		Message feedback = null;
		
		board.createBindings(parameters);

		return feedback;
	}	
	
	/**
	 * Calls the addPlayer function on the game board with a list of Players.
	 * @param players
	 *
	 */
	private Message insertPlayers( Vector parameters ) {
		Message feedback = null;

		for(int i = 0; i < parameters.size(); i++)
			board.addPlayer( (Cluedo.Game.Player)parameters.elementAt(i) );

		return feedback;
	}
	
	/** 
	 * Simply creates a new board calling the default constructor.
	 */
	private Message createBoard( Vector parameters ) {
		Message feedback = new Message();

		board = new Cluedo.Game.Board();
		
		feedback.type = "Board";
		feedback.data = "setup";
		
		return feedback;
	}
	
	
	/**
	 * Makes the board update the turn to the next player then the nextPlayer
	 * message makes all clients update.
	 *
	 */
	private Message nextPlayer() {
		Message feedback = new Message();
		
		board.nextPlayer();
		feedback.type = "nextPlayer";
		
		return feedback;
	}
	
	
	/**
	 * Puts players into their respective starting positions, these decided by
	 * looking at all the StartingSquare Positions.
	 *
	 **/
	private Message positionPlayers(String arrangement) {
		Message feedback = null;
		
		Vector players = board.getPlayers();
		Player currentPlayer = null;
		
		Vector startingNodes = new Vector();

		if(arrangement.equals("existing")) {
			for(int i = 0; i < players.size(); i++) {
				currentPlayer = (Player)players.elementAt(i);
				
//				System.out.println("[GC:positionPlayers] currentPlayer: "+currentPlayer);
				Position fakePosition = currentPlayer.getPosition();

				System.out.println(currentPlayer.getRealName()+" "+currentPlayer.getPosition().getID());
				if(fakePosition == null)
					System.out.println(currentPlayer.getRealName() +"  no loaded position.");
				else {
					currentPlayer.setPosition( getBoard().map.findByID( fakePosition.getID() ) );
					currentPlayer.getPosition().enterPlayer(currentPlayer);
					
				}
				
			}
		}
		else if(arrangement.equals("defined")) {
			
			Vector lostSheep = new Vector();
			
			
			for(int i = 0; i < board.map.getNodes().size(); i++) {
				if(board.map.getNodes().elementAt(i) instanceof StartingSquare) {
					startingNodes.add ( board.map.getNodes().elementAt(i) );
				}
			}

			for(int i = 0; i < players.size(); i++) {
				currentPlayer = (Player)players.elementAt(i);
				StartingSquare pos = null;
				
				for(int k = 0; k < startingNodes.size(); k++)
					if(((StartingSquare)startingNodes.elementAt(k)).getStartingPiece().equals(currentPlayer.getCharacter())) {
						pos = ((StartingSquare)startingNodes.elementAt(k));
						startingNodes.remove(pos);
					}

				if(pos != null) {
					currentPlayer.setPosition(pos);
					pos.enterPlayer( currentPlayer );
				}
				else {
					System.out.println("No place for " + currentPlayer.getCharacter() + " to start.");
					lostSheep.add(currentPlayer);
				}
			}
			
			if(lostSheep.size() > 0) {
				System.out.println("[GC:positionPlayers] Placing lost sheep");

			// loop to fill out any lost sheep!
				for(int i = 0; i < lostSheep.size(); i++) {
					currentPlayer = (Player)lostSheep.elementAt(i);
					StartingSquare pos = ((StartingSquare)startingNodes.firstElement());
					startingNodes.remove(pos);
			
					currentPlayer.setPosition(pos);
					pos.enterPlayer( currentPlayer );
				}
			}
			
			
		}
		
		return feedback;
	}

	/**
	 * Locate the card and eliminate it.
	 *
	 */
	private Message eliminateCard(String cardTarget) {
		Message feedback = null;

		Cluedo.Game.DetectivePad pad = board.getCurrentPlayer().getDetectivePad();
		pad.markCard(cardTarget);
		
		
		return feedback;
	}

	/**
	 * Locate the card and un eliminate it.
	 *
	 */
	private Message uneliminateCard(String cardTarget) {
		Message feedback = null;

		Cluedo.Game.DetectivePad pad = board.getCurrentPlayer().getDetectivePad();
		pad.unMarkCard(cardTarget);
		
		
		return feedback;
	}
	
	

	/* Chooses the cards that will be placed in the secret envelope for the beginning of the game.
	 *
	 * @upadtedby Zubair 9/12/04
	 * 
	 */
	private Message setEnvelopeCards( Vector parameters ) {
		Message feedback = new Message();
		
		//this is a copy of the CardCollection inthe parameters Vector
		CardCollection tmpCollection = (CardCollection) parameters.elementAt(0);
		CardCollection envelope = null;
		Vector weaponCards = new Vector();
		Vector characterCards = new Vector();
		Vector roomCards = new Vector();
		
		Card currentCard = new Card();
		for (int i=0; i < tmpCollection.countCards(); i++) {
			
			currentCard = ((Card) tmpCollection.cardAt(i));
		
			if (currentCard.getType().equals("Room")){
				roomCards.addElement(currentCard);
				
			}
			else if (currentCard.getType().equals("Suspect")){
				characterCards.addElement(currentCard);
			}
			else if (currentCard.getType().equals("Weapon")){
				weaponCards.addElement(currentCard);
			}
			else {
				System.out.println("[GameController:setEnvelopeCards] Invalid card type: "+ currentCard.getType());
			}
			
		}
		
		// Select random cards to go in the envelope
		int randomRoom;
			randomRoom = (int) (Math.random() * roomCards.size());
		int randomCharacter;
			randomCharacter = (int) (Math.random() * characterCards.size());
		int randomWeapon;
			randomWeapon = (int) (Math.random() * weaponCards.size());
			
		envelope = new CardCollection();
		envelope.insertCard((Card) roomCards.elementAt(randomRoom));
		envelope.insertCard((Card)characterCards.elementAt(randomCharacter));
		envelope.insertCard((Card) weaponCards.elementAt(randomWeapon));
		board.setEnvelope(envelope);
	
		
		feedback.type="newEnvelope";
		feedback.data= ((Card) roomCards.elementAt(randomRoom)).getName()+", "+
				((Card) characterCards.elementAt(randomCharacter)).getName()+", "+
				((Card) weaponCards.elementAt(randomWeapon)).getName();

		System.out.println("[GC:setEnvelopeCards] Winning cards: " + feedback.data);

		return feedback;
	}
	
	/**
	 * Called after a save game
	 * 
	 * @param collection
	 * @return
	 */
	private Message restoreEnvelopeCards(CardCollection collection) {
		Message feedback = null;
		
		board.setEnvelope(collection);
		
		System.out.println("restoreEnvelopeCards: "+board.getEnvelope().toXML());

		return feedback;
	}
	

	/*
	 * Deals cards to players in a random way.
	 *
	 * @upadtedby Zubair 13/12/04
	 * 
	 */
	 private Message dealCardsToPlayers() {
		Message feedback = null;

		CardCollection cardsForDealing = board.getCardPack();
	  
		// Remove all the cards in the envelope from the cards for dealing	  
		for (int i = 0; i < board.getEnvelope().countCards(); i++) {
			cardsForDealing.removeCard( board.getEnvelope().cardAt(i) );
		}
	   

		int random = 0;
		Vector localPlayers = board.getPlayers();
		int playerMarker = 0;
		Player currentPlayer = null;
		
		try {
			while (cardsForDealing.countCards() > 0) {

				// Get a random number in the card pack
				random = (int) (Math.random() * cardsForDealing.countCards());
				
				// Find the next player object
				currentPlayer = (Player)localPlayers.elementAt(playerMarker);
				
				currentPlayer.getDetectivePad().markCard(cardsForDealing.cardAt(random).getName());

				// Give the card to the player
				currentPlayer.giveCard(cardsForDealing.cardAt(random));
				
				// Remove the card just dealt from pack of dealing cards 
				cardsForDealing.removeCard(cardsForDealing.cardAt(random));
				
				// Go to the next player
				playerMarker++;
				
				if(playerMarker == localPlayers.size())
					playerMarker = 0;
			}
		}
		catch (Exception error) {
			System.out.println("[GameController:dealCardsToPlayer] Card is invalid.");
			System.out.println("[GameController:dealCardsToPlayer] Fatal exit.");
			System.exit(-1);
		}

		return feedback;
	}

}
	
